#ifndef SIMPLE_SUPPORT_TUPLE_UTILS_APPLY_FOR_HPP
#define SIMPLE_SUPPORT_TUPLE_UTILS_APPLY_FOR_HPP

#include <tuple> // std::tuple_size_v std::tuple_element_t std::get
#include <type_traits> // std::remove_reference_t
#include <utility> // std::forward
#include <cassert> // assert
#include "../range.hpp" // range
#include "../function_utils.hpp" // invoke
#include "../meta/misc.hpp" // meta::nop_t

namespace simple::support
{

	// TODO: return a variant if necessary
	// TODO: can have multiple tuples here as well
	template<size_t I, template<typename> class Get = meta::nop_t, typename Value, typename F, typename Tuple>
	constexpr
	decltype(auto) apply_for(Value value, F&& f, Tuple&& tuple)
	{
		using tuple_t = std::remove_reference_t<Tuple>;
		constexpr auto size = std::tuple_size_v<tuple_t>;
		using element_t = std::tuple_element_t<I,tuple_t>;
		static_assert(I < size);

		using std::get;
		if(Get<element_t>::value == value)
			return support::invoke(std::forward<F>(f),
				get<I>(std::forward<Tuple>(tuple)));

		// would have been more c++ish to go one past the end and make that a nop,
		// but then the return type deduction fails
		if constexpr (I != size-1)
			return apply_for<I + 1, Get>(value, std::forward<F>(f),
				std::forward<Tuple>(tuple));

		assert(!"simple::support::apply_for - no matching type value");
		// std::unreachable(); // TODO: c++23
	}

	template<template<typename> class Get = meta::nop_t, typename Value, typename F, typename Tuple>
	constexpr
	decltype(auto) apply_for(Value value, F&& f, Tuple&& tuple)
	{ return apply_for<0,Get>(value, std::forward<F>(f), std::forward<Tuple>(tuple)); }

	// TODO: return a variant if necessary
	template<size_t I, typename F, typename First, typename... Rest>
	constexpr
	decltype(auto) apply_to(size_t index, F&& f, First&& first, Rest&&... rest)
	{
		constexpr auto size = std::tuple_size_v<std::remove_reference_t<First>>;
		static_assert(I < size);

		using std::get;
		if(I == index)
			return support::invoke(std::forward<F>(f),
				get<I>(std::forward<First>(first)),
				get<I>(std::forward<Rest>(rest))...
			);

		// would have been more c++ish to go one past the end and make that a nop,
		// but then the return type deduction fails
		if constexpr (I != size-1)
			return apply_to<I + 1>(index, std::forward<F>(f),
				std::forward<First>(first), std::forward<Rest>(rest)...);

		assert(!"simple::support::apply_to - index out of bounds");
		// std::unreachable(); // TODO: c++23
	}

	template<typename F, typename First, typename... Rest>
	constexpr
	decltype(auto) apply_to(size_t index, F&& f, First&& first, Rest&&... rest)
	{
		return apply_to<0>(index, std::forward<F>(f),
			std::forward<First>(first), std::forward<Rest>(rest)...);
	}

	// TODO: optionally specify an output iterator or return a static vector
	template<typename F, typename First, typename... Rest>
	constexpr
	void apply_to(range<size_t> index_range, F&& f, First&& first, Rest&&... rest)
	{
		for(size_t i = index_range.lower(); i < index_range.upper(); ++i)
			apply_to(i, std::forward<F>(f),
				std::forward<First>(first), std::forward<Rest>(rest)...);
	}

} // namespace simple::support

#endif /* end of include guard */
